1 Overview

CONSORT diagrams depict “progress through the phases of a parallel randomized trial of two groups (that is, enrollment, intervention allocation, follow-up, and data analysis)” (http://www.consort-statement.org/consort-statement/flow-diagram), and are often required with the submission of a publication.

Word and pdf templates of a CONSORT diagram are available, but manually filling these out leaves a lot to be desired; it loses reproducibility, is error prone, and it becomes tedious when having to update an analysis.

One possible solution is to use Graphviz, a data visualization software along with the available integration into R and RStudio and glue for string interpolation.

2 Graphviz and R/RStudio

The DiagrammeR package brings integration of Graphviz and mermaid.js into R, allowing diagrams to easily be created and rendered. For more information, there’s a great blog post by RStudio here.

In short, RStudio has a built in editor for .dot and .mmd files that can then be rendered by DiagrammeR:

Graphviz code and output in RStudio

Figure 2.1: Graphviz code and output in RStudio

dot and mmd code can also be written directly in R code and the passed as a string to DiagrammeR::grViz or DiagrammeR::mermaid:

DiagrammeR::grViz("
digraph t {
  rankdir='LR';
  hello -> world
}             
")

One major disadvantage here is you lose out on some of the convenience factors of a text editor that supports a language (e.g. syntax highlighting, auto-complete, and debugging tools).

3 Variable Interpolation

To get the full benefit of building a consort diagram programmatically, we need the ability to fill in values based on a variable derived in R. DiagrammeR has built in support for string substitution, but the syntax is little clunky and creates a dependency on DiagrammeR::grViz to render a diagram.

Another solution would be to use the string interpolation capabilities of the R package glue.

Note that in Figure 2.1, sample size values were wrapped in < >. This served two purposes:

  • Placeholders during development

  • Indicators for glue to interpolate a value

Note, by default, glue uses curly brackets for the opening and closing delimiters. <> was chosen because they aren’t as common in the DOT language.

greeting <- "Hello"
name <- "John"
glue::glue("{greeting}, {name}. How are you?")
## Hello, John. How are you?

With this setup, we can read in a .dot file and interpolate the values to something we calculated in R.

4 CONSORT Example

Let’s say we create a file named consort.dot in our working directory with the following contents:

digraph consort
{
  graph [pad = 0.2, nodesep = .5, ranksep=.6]
  node [fontname = Helvetica, shape = box, width = 6, margin = .2]
  assessed [label = "Assessed for eligibility (n = <assessed>)", width = 4]
  excluded [label = "Excluded (n = <excluded_total>) \l" +
                   "• Not meeting inclusion criteria (n = <excluded_crit>)\l" +
                   "• Declined to participate (n = <excluded_dec>)\l" +
                   "• Other reasons (n = <excluded_oth>)\l",
           width = 4]
  randomized [label = "Randomized (n = <randomized>)", width = 3]
  treatment [label = "Allocated to intervention (n = <interv_total>)\l" +
                      "• Received allocated intervention (n = <interv_rec>)\l" +
                      "• Did not receive allocated intervention (give reasons) (n = <interv_no>)\l"]
  control [label = "Allocated to intervention (n = <interv_total>)\l" +
                      "• Received allocated intervention (n = <interv_rec>)\l" +
                      "• Did not receive allocated intervention (give reasons) (n = <interv_no>)\l"]
  lostC [label = "Lost to follow-up (give reasons) (n = <lost_fu>)\l\l" +
                 "Discontinued intervention (give reasons) (n = <disc>)\l"]
  lostT [label = "Lost to follow-up (give reasons) (n = <lost_fu>)\l\l" +
                 "Discontinued intervention (give reasons) (n = <disc>)\l"]
  analyzedC [label = "Analyzed (n = <analyzed>)\l" +
                     "• Excluded from analysis (give reasons) (n = 0)\l"]
  analyzedT [label = "Analyzed (n = <analyzed>)\l" +
                     "• Excluded from analysis (give reasons) (n = 0)\l"]
  blank [label = "", width = 0.01, height = 0.01]
  
  { rank = same; blank excluded }
  
  assessed -> blank[dir = none];
  randomized -> {treatment control};
  blank -> excluded[minlen = 3];
  blank -> randomized;
  treatment -> lostT;
  control -> lostC;
  lostC -> analyzedC;
  lostT -> analyzedT;
  
}

Then, we derive some values to fill the diagram in with. These normally would already be derived in your analysis.

assessed <- 1000

excluded_total <- 100
excluded_crit <- 70
excluded_dec <- 20
excluded_oth <- 10

randomized <- 900

interv_total <- 450
interv_rec <- 400
interv_no <- 50

lost_fu <- 23
disc <- 2

analyzed <- interv_total - lost_fu - disc

Finally, we read in the file, interpolate the values with glue, then render with DiagrammeR.

readr::read_file("consort.dot") %>% 
  glue::glue(.open = "<", .close = ">") %>% 
  DiagrammeR::grViz()